package graphics.Shapes;


import graphics.Meshes.AbstractMesh;
import graphics.Meshes.Edge;
import graphics.Meshes.Trigon;

import java.util.ArrayList;
import java.util.HashMap;

import javax.media.opengl.GL;

import org.eclipse.swt.graphics.ImageData;

import common.Math3D;
import common.Parameterization2D;
import common.Point3D;
import common.SphereMap;

public class Sphere extends AbstractMesh {

	int subdivisionLevel = 0;
	double radius = 2;

	double heightFactor = 0.2;
	boolean withHeightMap = false;;
	SphereMap heightMap = null;

	
	private int trigonTilesLevel = 0;

	HashMap<Trigon, TrigonParameterization> trigonsParameterizations;

	public void setParameter(String name, String args[])
	{
		if (name.equals("subdivisionLevel")) {
			setSubdivisionLevel(Integer.parseInt(args[0]));	
			return;
		}

		if (name.equals("radius")) {
			this.setRadius(Double.parseDouble(args[0])); 	
			return;
		}

		if (name.equals("heightImage")) {
			heightMap = new SphereMap(new ImageData(args[0]));
			withHeightMap = true;
			return;
		}

		if (name.equals("heightFactor")) {
			setHeightFactor(Float.parseFloat(args[0]));	
			return;
		}

		if (name.equals("tilesSize")){
			trigonTilesLevel = Integer.parseInt(args[0]);
		}

		super.setParameter(name, args);
	}

	@Override
	public void initialize(GL gl){

		Point3D p0 = new Point3D(0,1,0);
		Point3D q0 = new Point3D(0,-1,0);
		p0.normalize(surfaceHeight(parameterizePoint(p0)));
		q0.normalize(surfaceHeight(parameterizePoint(q0)));
		this.getPoints().add(p0);
		this.getPoints().add(q0);

		Point3D ppoints[] = new Point3D[5];
		Point3D qpoints[] = new Point3D[5];

		double angle = 2.0 * Math.PI / 5;
		double midradius = radius * Math.cos(Math.PI/6);
		for (int i = 0; i < 5; i++){

			ppoints[i] = new Point3D(midradius * Math.cos(i*angle),
					0.5 * radius ,
					midradius * Math.sin(i*angle));
			qpoints[i] = new Point3D(
					midradius * Math.cos((i+0.5)*angle),
					-0.5 * radius,
					midradius * Math.sin((i + 0.5)*angle));
			ppoints[i].normalize(surfaceHeight(parameterizePoint(ppoints[i])));
			qpoints[i].normalize(surfaceHeight(parameterizePoint(qpoints[i])));
			this.getPoints().add(ppoints[i]);
			this.getPoints().add(qpoints[i]);

		}

		trigonsParameterizations = new HashMap<Trigon, TrigonParameterization>();

		for (int i = 0; i < 5; i++){
			Trigon trigon1 = new Trigon(p0,ppoints[(i+1)%5],ppoints[i]);
			Trigon trigon2 = new Trigon(q0,qpoints[i],qpoints[(i+1)%5]);
			Trigon trigon3 = new Trigon(qpoints[i],ppoints[i],ppoints[(i+1)%5]);
			Trigon trigon4 = new Trigon(qpoints[(i+1)%5],qpoints[i],ppoints[(i+1)%5]);

			Parameterization2D r0 = new Parameterization2D(0.5, 0);
			Parameterization2D r1 = new Parameterization2D(0, 1);
			Parameterization2D r2 = new Parameterization2D(1, 1);

			trigonsParameterizations.put(trigon1, new TrigonParameterization(r0,r2,r1));
			trigonsParameterizations.put(trigon3, new TrigonParameterization(r0,r1,r2));
			trigonsParameterizations.put(trigon4, new TrigonParameterization(r2,r1,r0));
			trigonsParameterizations.put(trigon2, new TrigonParameterization(r0,r1,r2));

			getTrigons().add(trigon1);
			getTrigons().add(trigon2);
			getTrigons().add(trigon3);
			getTrigons().add(trigon4);
		}



		for (int i = 0; i < subdivisionLevel; i++){

			subDivide(i);
		}

		super.initialize(gl);

		if (primaryTrigonsTexture || secondaryTrigonsTexture){
			trigonsParameterizations.clear();
		}
		Runtime.getRuntime().gc();
	}
	protected void subDivide(int i){
		ArrayList<Trigon> midtrigons = new ArrayList<Trigon>();
		HashMap<Point3D, HashMap<Point3D, Point3D>> midpoints = new HashMap<Point3D, HashMap<Point3D,Point3D>>();
		ArrayList<Point3D> pointsToAdd = new ArrayList<Point3D>();
		for (Trigon trigon : trigons){

			Point3D m01 = getMidPoint(midpoints, trigon.p0, trigon.p1, pointsToAdd);
			Point3D m12 = getMidPoint(midpoints, trigon.p1, trigon.p2, pointsToAdd);
			Point3D m20 = getMidPoint(midpoints, trigon.p2, trigon.p0, pointsToAdd);

			//if (m01 == null || m12 == null || m20 == null)
				//System.out.println("eeadfsadfsd");
			
			Trigon t0 = new Trigon(trigon.p0,m01,m20);
			Trigon t1 = new Trigon(trigon.p1,m12,m01);
			Trigon t2 = new Trigon(trigon.p2,m20,m12); 
			Trigon tm = new Trigon(m01,m12,m20);
			midtrigons.add(t0);
			midtrigons.add(t1);
			midtrigons.add(t2);
			midtrigons.add(tm);

			if (primaryTrigonsTexture || secondaryTrigonsTexture){
				TrigonParameterization tp = trigonsParameterizations.get(trigon);
				if (tp == null){
					System.out.println("err");
				}
				trigonsParameterizations.remove(trigon);

				if (i > trigonTilesLevel){
					Parameterization2D pr01 = new Parameterization2D(
							(tp.p0.getX() + tp.p1.getX())/2.0, 
							(tp.p0.getY() + tp.p1.getY())/2.0);
					Parameterization2D pr12 = new Parameterization2D(
							(tp.p1.getX() + tp.p2.getX())/2.0, 
							(tp.p1.getY() + tp.p2.getY())/2.0);
					Parameterization2D pr20 = new Parameterization2D(
							(tp.p2.getX() + tp.p0.getX())/2.0, 
							(tp.p2.getY() + tp.p0.getY())/2.0);
					trigonsParameterizations.put(t0,new TrigonParameterization
							(tp.p0, pr01, pr20));
					trigonsParameterizations.put(t1,new TrigonParameterization
							(tp.p1, pr12, pr01));
					trigonsParameterizations.put(t2,new TrigonParameterization
							(tp.p2, pr20, pr12));
					trigonsParameterizations.put(tm,new TrigonParameterization
							(pr01, pr12, pr20));
				}else{

					trigonsParameterizations.put(t0,new TrigonParameterization
							(tp.p0, tp.p1, tp.p2));
					trigonsParameterizations.put(t1,new TrigonParameterization
							(tp.p1, tp.p2, tp.p0));
					trigonsParameterizations.put(t2,new TrigonParameterization
							(tp.p2, tp.p0, tp.p1));
					trigonsParameterizations.put(tm,new TrigonParameterization
							(tp.p1, tp.p0, tp.p2));

				}
			}
		}

		this.points.addAll(pointsToAdd);
		this.trigons.clear();
		this.trigons.addAll(midtrigons);
	}


	protected double surfaceHeight(Parameterization2D pmz){
		if (isWithHeightMap()){
			return radius + getHeightFactor() * getHeightMap().sampleBilinear(pmz);
		}
		return radius;
	}
	public double surfaceHeight(Point3D point){
		return surfaceHeight(parameterizePoint(point));
	}

	private double calcWidthAngle(Point3D point){
		Point3D v = point.clone();
		v.y = 0;
		double angle;
		if (v.x == 0){ 
			angle = 0;
		}
		else{
			v.normalize();
			angle = Math.acos(Math3D.getDotProduct(new Point3D(1, 0, 0), v));
		}
		if (point.z >= 0) return angle;
		else return 2.0 * Math.PI - angle;
	}

	private double calcHeightAngle(Point3D point){
		Point3D v = point.clone();
		v.normalize();
		return Math.acos(Math3D.getDotProduct(new Point3D(0, 1, 0), v));
	}

	@Override
	public Parameterization2D parameterizePoint(Point3D point){
		return new Parameterization2D ( calcWidthAngle(point) / (2*Math.PI) / 2, calcHeightAngle(point) / Math.PI );
	}

	@Override
	public void reparametrize(Parameterization2D p0, Parameterization2D p1, Parameterization2D p2,Trigon trigon, boolean tiles){

		if (!tiles)
		{
			if ((p0.getX() + 0.4 < p1.getX()) && (p0.getX() < 0.5)) p0.setX(p0.getX() + 0.5);
			if ((p0.getX() + 0.4 < p2.getX()) && (p0.getX() < 0.5)) p0.setX(p0.getX() + 0.5);
			if ((p1.getX() + 0.4 < p0.getX()) && (p1.getX() < 0.5)) p1.setX(p1.getX() + 0.5);
			if ((p1.getX() + 0.4 < p2.getX()) && (p1.getX() < 0.5)) p1.setX(p1.getX() + 0.5);
			if ((p2.getX() + 0.4 < p0.getX()) && (p2.getX() < 0.5)) p2.setX(p2.getX() + 0.5);
			if ((p2.getX() + 0.4 < p1.getX()) && (p2.getX() < 0.5)) p2.setX(p2.getX() + 0.5);
		}


		if (tiles)	{
			p0.setX(trigonsParameterizations.get(trigon).p0.getX());
			p0.setY(trigonsParameterizations.get(trigon).p0.getY());
			p1.setX(trigonsParameterizations.get(trigon).p1.getX());
			p1.setY(trigonsParameterizations.get(trigon).p1.getY());
			p2.setX(trigonsParameterizations.get(trigon).p2.getX());
			p2.setY(trigonsParameterizations.get(trigon).p2.getY());
		}
	}

	@Override
	public Point3D calculateMidpoint(Edge edge) {
		Point3D mp = Math3D.getAveragePoint(edge.getP0(), edge.getP1());
		double dist = surfaceHeight(parameterizePoint(mp));
		mp.normalize(dist);
		return mp;
	}

	public int getSubdivisionLevel() {
		return subdivisionLevel;
	}

	public void setSubdivisionLevel(int subdivisionLevel) {
		this.subdivisionLevel = subdivisionLevel;
	}

	public double getRadius() {
		return radius;
	}

	public void setRadius(double radius) {
		this.radius = radius;
	}

	public double getHeightFactor() {
		return heightFactor;
	}
	public void setHeightFactor(double heightFactor) {
		this.heightFactor = heightFactor;
	}
	public SphereMap getHeightMap() {
		return heightMap;
	}
	public void setHeightMap(SphereMap heightMap) {
		this.heightMap = heightMap;
	}
	public boolean isWithHeightMap() {
		return withHeightMap;
	}

	public void setWithHeightMap(boolean withHeightMap) {
		this.withHeightMap = withHeightMap;
	}


	//	private void tilesParameterization(Parameterization2D p0, Parameterization2D p1, Parameterization2D p2){
	//		//((double) Math.pow(2, subdivisionLevel)); //needs to be larger than polygons
	//		
	//		Parameterization2D leftmost = p0;
	//		if (leftmost.getX() > p1.getX()) leftmost = p1;
	//		if (leftmost.getX() > p2.getX()) leftmost = p2;
	//		
	//		Parameterization2D topmost = p0;
	//		if (topmost.getY() > p1.getY()) topmost = p1;
	//		if (topmost.getY() > p2.getY()) topmost = p2;
	//		
	//		tilesParameterizationFix(p0, tileSize);
	//		tilesParameterizationFix(p1, tileSize);
	//		tilesParameterizationFix(p2, tileSize);
	//		
	//		if (p0.getX() < leftmost.getX()) p0.setX(p0.getX() + 0.5);
	//		if (p1.getX() < leftmost.getX()) p1.setX(p1.getX() + 0.5);
	//		if (p2.getX() < leftmost.getX()) p2.setX(p2.getX() + 0.5);
	//
	//		if (p0.getY() < topmost.getY()) p0.setY(p0.getY() + 0.5);
	//		if (p1.getY() < topmost.getY()) p1.setY(p1.getY() + 0.5);
	//		if (p2.getY() < topmost.getY()) p2.setY(p2.getY() + 0.5);
	//		
	//		return;
	//	}

	//	private void tilesParameterizationFix(Parameterization2D p, double size){
	//		p.setX(p.getX() * 4);
	//		int sqx = (int)(p.getX() / size);
	//		p.setX(p.getX() - ((double)sqx * size));
	//		
	//		int sqy = (int)(p.getY() / size);
	//		p.setY(p.getY() - ((double)sqy * size));
	//	}

}

class TrigonParameterization{
	Parameterization2D p0,p1,p2;

	public TrigonParameterization(
			Parameterization2D p0,
			Parameterization2D p1,
			Parameterization2D p2)
	{
		this.p0 = p0;
		this.p1 = p1;
		this.p2 = p2;
	}
}
